package bsserver;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Object instantiated with the absolute pathname of given file.
 * Methods return authorization information about specified file.
 * @author marc
 */
public class Auth {

    private String AbsolutePath;
    private final static Vector<String> authKeyWords = new Vector<String>();
    private boolean AccessFileProtected = false;
    private boolean DirectoryProtected = false;
    private boolean IsReadable = false;
    private boolean IsWriteable = false;
    private boolean IsExecutable = false;
    private boolean IsDirectoryExecutable = false;
    private boolean IsDirectoryReadable = false;
    private boolean IsDirectoryWritable = false;
    private String ProtectedPath = "";
    private String CurrentDirectory;
    private String CurrentFile;

    /* The bsserver supports the following tags in either <directory> directive
     * in httpd.conf or in .htaccess file:
     *
     * AuthType:
     *  * Basic
     * AuthName:
     *  * any string
     * AuthUserFile:
     *  * filename of user file.  absolute path required
     * Require
     *  * valid-user: any valid username in AuthUserFile.
     *  * user username[ username][...] specific user(s) in AuthUserFile.
     * Since no AuthGroup file was part of the spec, it is assumed that
     * it is not to be implemented.  So it wasn't.
     *
     */
    private String AuthType;
    private String AuthName;
    private String AuthUserFile;
    private boolean AuthAllInFile = false;
    private Vector<String> AuthUsers = new Vector<String>();
    private String Require;
    private String AuthorizedUser;


    /**
     * Creates a new Auth object to check authorization permissions on the given
     * path
     * @param absPath the path to check permissions for
     */
    public Auth(String absPath) {

        this.changeDirectory(absPath);

    }

    /**
     * Returns the access filename relevent to directory.  If AccessFileName
     * not specified, .htaccess will, by default, be searched for.
     *
     * Will search from current specified directory all the way to server root
     * returning the first file found.  Returns null if none found, which
     * implies the directory is not password protected.
     *
     * From <tt>http://httpd.apache.org/docs/1.3/howto/htaccess.html</tt>
     * <h2>How directives are applied</h2>
     * <blockquote>
     * The configuration directives found in a .htaccess file are applied
     * to the directory in which the .htaccess file is found, and to all
     * subdirectories thereof. However, it is important to also remember
     * that there may have been .htaccess files in directories higher up.
     * Directives are applied in the order that they are found. Therefore,
     * a .htaccess file in a particular directory may override directives
     * found in .htaccess files found higher up in the directory tree.
     * And those, in turn, may have overridden directives found yet
     * higher up, or in the main server configuration file itself.
     * </blockquote>
     * For bsserver, therefore: the closest .htaccess file to the directory
     * that we are requesting from has precedence.
    */
    /**
     * Changes the current directory to which this auth object is
     * referring.  Recalculates system permissions, and tests
     * to see if authentication is required
     * @param absPath absolute pathname to file
     */
    public void changeDirectory(String absPath) {

        String pathString = absPath;
        File absPathFile = new File(absPath);

        CurrentDirectory = absPath;

        AbsolutePath = absPath;

        // make sure there is an abolute path
        if (!absPathFile.isAbsolute()) {

            System.out.println("Error: Auth.changeDirectory must get an absolute pathname.");
            System.exit(1);
        }

        // get system permissions for directory
        // don't even bother to proceed if the directory is not
        // executable, which it must be for all operations
        checkSystemPermissions();

        if (!IsDirectoryExecutable) {
            return;
        }

        CurrentFile = absPathFile.getName();

        // cut out file, get remaining directory
        pathString = absPathFile.getParentFile().getAbsolutePath();
        absPathFile = new File(pathString);

        // Go through each directory leading to the specified file.
        // make sure no trailing /'s

        String myServerRoot = Main.HttpdConfig.get("DocumentRoot");
        if (myServerRoot.endsWith("/")) {
            myServerRoot = myServerRoot.substring(0, myServerRoot.length() - 1);
        }

        // make a list of the directories to check, all the way
        // to the server root
        Vector<String> pathsToCheck = new Vector<String>();

        while (!pathString.equals(myServerRoot) && pathString.length() != 0) {
            pathsToCheck.add(pathString);

            // if we're already at root, get out
            if (absPathFile.toString().equals("/")) {
                break;
            } else {
                pathString = absPathFile.getParentFile().getAbsolutePath();
                absPathFile = new File(pathString);
            }
        }
        // don't forget the root!
        pathsToCheck.add(pathString);

        // iterator preservers the order of the vector
        String currentPath = new String();
        for (String pathToCheck : pathsToCheck) {
            currentPath = pathToCheck;

            // Directory protected by AuthFilename trumps directory
            // proteced by httpd.conf
            if (new File(currentPath + "/" + Main.HttpdConfig.get("AuthFilename")).exists()) {
                ProtectedPath = currentPath;
                AccessFileProtected = true;
                break;
            } else {
                if (Main.HttpdConfig.getDirectoryAccess(currentPath) != null) {
                    ProtectedPath = currentPath;
                    DirectoryProtected = true;
                    break;
                } else {
                    
                }
            }
        }

        // Now we know if the directory is password protected.
        // Scan through either the file or the vector, placing
        // all the entries into a string so we can use stremtokenizer
        // to parse out values


        String dirAccessString = "";

        if (DirectoryProtected) {

            for(String dirTok : Main.HttpdConfig.getDirectoryAccess(currentPath)){
                dirAccessString = dirAccessString + dirTok + "\n";
                
            }



        } else if (AccessFileProtected) {
            // If it is a file protecting the directory, open the file,
            // place all its contents in a string.

            BufferedReader reader = null;
            try {
                reader = new BufferedReader(
                        new FileReader(currentPath + "/" + Main.HttpdConfig.get("AuthFilename")));

            } catch (FileNotFoundException e) {
                System.out.println("Error: file not found reading access file: " + currentPath + "/" + Main.HttpdConfig.get("AuthFilename"));
                AccessFileProtected = false;
                return;
            }

            try {
                String in;
                while ((in = reader.readLine()) != null) {
                    dirAccessString = dirAccessString.concat(in + "\n");
                }
                reader.close();

            } catch (IOException e) {
                System.out.println("Error occurred reading access file: " + currentPath + "/" + Main.HttpdConfig.get("AuthFilename"));
                System.exit(1);
            }
        }

        // dirAccessString now has all values from either vector or file
        // tokenize stream of this string to get values

        StringReader strTok = new StringReader(dirAccessString);

        StreamTokenizer s = new StreamTokenizer(strTok);

        s.resetSyntax();
        s.whitespaceChars(1, 32);
        s.wordChars(33, 126);
        s.quoteChar('"');
        s.commentChar('#');
        s.eolIsSignificant(true);
        s.slashSlashComments(false);
        s.slashStarComments(false);

        String keyword;

        try {
            while (s.nextToken() != StreamTokenizer.TT_EOF) {

                keyword = s.sval;

                boolean authKeyWordsSyntaxError = false;
                String authKeyWordsErrorMessage = "";
                if (authKeyWords.contains(keyword)) {

                    if (s.nextToken() != StreamTokenizer.TT_EOF) {

                        if (keyword.equals("AuthType")) {
                            if (s.sval.equals("Basic")) {
                                AuthType = "Basic";

                                if (s.nextToken() != StreamTokenizer.TT_EOL) {
                                    authKeyWordsSyntaxError = true;
                                    authKeyWordsErrorMessage = "Missing EOL after AuthType";
                                }

                            } else {
                                authKeyWordsSyntaxError = true;
                                authKeyWordsErrorMessage = "Basic is the only AuthType recognized";
                            }
                        } else if (keyword.equals("AuthName")) {
                            AuthName = s.sval;
                            if (s.nextToken() != StreamTokenizer.TT_EOL) {
                                authKeyWordsSyntaxError = true;
                                authKeyWordsErrorMessage = "Missing EOL after AuthName";
                            }
                        } else if (keyword.equals("AuthUserFile")) {
                            AuthUserFile = s.sval;

                            if (s.nextToken() != StreamTokenizer.TT_EOL) {
                                authKeyWordsSyntaxError = true;
                                authKeyWordsErrorMessage = "Missing EOL after AuthUserFile...is your directory in qutoes?";

                            }
                        } else if (keyword.equals("Require")) {
                            if (s.sval.equals("valid-user")) {
                                int requireKeywordTokStat = s.nextToken();

                                AuthUsers.add("valid-user");
                                // toss out any addional information on the valid-user line
                                Require = "valid-user";
                                while (requireKeywordTokStat != StreamTokenizer.TT_EOF &&
                                        requireKeywordTokStat != StreamTokenizer.TT_EOL) {
                                    requireKeywordTokStat = s.nextToken();
                                }
                            } else if (s.sval.equals("user")) {
                                Require = "user";

                                int requireKeywordTokStat = s.nextToken();

                                // add user names to the vector
                                while (requireKeywordTokStat != StreamTokenizer.TT_EOF &&
                                        requireKeywordTokStat != StreamTokenizer.TT_EOL) {
                                    AuthUsers.add(s.sval.toString());
                                    requireKeywordTokStat = s.nextToken();
                                }
                            } else {
                                authKeyWordsSyntaxError = true;
                                 authKeyWordsErrorMessage = "Unrecognized stuff after user";

                            }
                        } else {
                            authKeyWordsSyntaxError = true;
                            authKeyWordsErrorMessage = "Unrecognized Keyword";
                        }
                    }

                    if (authKeyWordsSyntaxError) {
                        System.out.print("Syntax error in ");
                        if(AccessFileProtected){
                            System.out.print(Main.HttpdConfig.get("AuthFilename") + ": ");
                        } else {
                            System.out.print("<directory> directive : ");
                        }
                        System.out.println(authKeyWordsErrorMessage);
                        if (AccessFileProtected) {
                            AccessFileProtected = false;
                            return;
                        } else {
                            DirectoryProtected = false;
                            return;
                        }
                    }
                }
            }



            /* Debugging, uncomment for information
            
            System.out.println("Authorization Information:");

            System.out.println("           IsReadable: " + IsReadable);
            System.out.println("          IsWriteable: " + IsWriteable);
            System.out.println("         IsExecutable: " + IsExecutable);
            System.out.println("  IsDirectoryReadable: " + IsDirectoryReadable);
            System.out.println("  IsDirectoryWritable: " + IsDirectoryWritable);
            System.out.println("IsDirectoryExecutable: " + IsDirectoryExecutable);


            System.out.println("AccessFileProtected: " + AccessFileProtected);
            System.out.println(" DirectoryProtected: " + DirectoryProtected);
            System.out.println("      ProtectedPath: " + ProtectedPath);
            System.out.println("   CurrentDirectory: " + CurrentDirectory);
            System.out.println("        CurrentFile: " + CurrentFile);
            System.out.println("           AuthType: " + AuthType);
            System.out.println("           AuthName: " + AuthName);
            System.out.println("       AuthUserFile: " + AuthUserFile);
            System.out.println("      AuthAllInFile: " + AuthAllInFile);
            System.out.println("            Require: " + Require);
            System.out.println("          AuthUsers:");
            Iterator authUsersIter = AuthUsers.iterator();
            while (authUsersIter.hasNext()) {
                System.out.println("                  *: " + authUsersIter.next());
            }
            */

        } catch (IOException ex) {
            Logger.getLogger(Auth.class.getName()).log(Level.SEVERE, null, ex);

        }

    }

    /**
     * Returns true if the reason file is proteced due to .htaccess file
     * (or AccessFileName) file protected.
     * @return
     */
    public boolean isAccessFileProtected() {
        return AccessFileProtected;
    }

    /**
     * Returns true if the reason file is protected due to <directory>
     * directive in httpd.conf.
     * @return true if <directory> caused protection, false otherwise
     */
    public boolean isDirectoryProteted() {

        return DirectoryProtected;
    }

    /**
     * Accepts a base64 encoded combination of username:password, checks
     * if the user/password combination is valid.
     * @param userPassword combined base64 encoded username:password
     * @return true if the user is valid, false if the user is not valid
     */
    public boolean checkBase64UserPassword(String userPassword) {
        // decode base 64 into username:password

        Base64Decoder userPasswordDecoder = new Base64Decoder(userPassword);

        String userPasswordDecoded = "";

        try {
            userPasswordDecoded = userPasswordDecoder.processString();
        } catch (Base64FormatException ex) {
            Logger.getLogger(Auth.class.getName()).log(Level.SEVERE, null, ex);
        }


        return this.checkUserPassword(userPasswordDecoded.substring(0, userPasswordDecoded.indexOf(":")),
                userPasswordDecoded.substring(userPasswordDecoded.indexOf(":") + 1, userPasswordDecoded.length()));

    }

    /**
     * Checks the current directory against passed user and password
     * @param user username to check if password protected
     * @param password password to check againt file to see if protected
     * @return true if the user/password combination valid, false if not.
     */
    public boolean checkUserPassword(String user, String password) {

        // if the directory is not password protected, any user is valid
        if (!AccessFileProtected && !DirectoryProtected) {
            return true;
        }

        BufferedReader reader = null;
        
        if(AuthUserFile == null){
            return false;
        }

        try {
            reader = new BufferedReader(new FileReader(AuthUserFile));
        } catch (FileNotFoundException ex) {

            System.out.println("Error: file not found reading user file: " + AuthUserFile);
            AccessFileProtected = false;
            return false;
        }

        try {
            String in;
            while ((in = reader.readLine()) != null) {

                if(in.trim().length() == 0){
                    continue;
                }

                if(in.indexOf(":")==-1){
                    System.out.println("Syntax error in password file " + AuthUserFile);
                    return false;
                }


                if (in.substring(0, in.indexOf(":")).equals(user)) {

                    String jCryptKeeper = JCrypt.crypt(in.substring(in.indexOf(":") + 1, in.indexOf(":") + 3), password);

                    if (jCryptKeeper.equals(in.substring(in.indexOf(":") + 1, in.length()))) {
                        AuthorizedUser = user;
                        reader.close();
                        return true;
                    }

                }

            }
            reader.close();

        } catch (IOException e) {
            System.out.println("Error occurred reading user file: " + AuthUserFile);

            AuthorizedUser = null;
            return false;
        }
        return false;
    }


    /**
     * Tells if the directory specified at instatiation or with changeDirectory
     * is password protected by either a <directory> directive or .htaccess file
     * @return true if protected by <directory> directive or .htaccess file false if not
     */
    public boolean isPasswordProtected() {
        // Searches directory tree from current directory up, looking
        // first for AuthFileName, and if none found, then for
        // <directory> directive for that directory.  If clear all the
        // way up to the document root, then there is no password protection
        // on the directory.

        if (AccessFileProtected || DirectoryProtected) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Tells if file specified in insatiation or with changeDirectory
     * is readable given system permissions.
     * @return true if readable false if not readable
     */
    public boolean isReadable() {
        // checks if all directories to and including root are executable
        // and if document is readable

        return IsReadable;
    }

    /**
     * Tells if file specified in insatiation or with changeDirectory
     * is writable given system permissions.
     * @return true if writeable false if not writable
     */
    public boolean isWriteable() {

        return IsWriteable;
    }

    /**
     * Tells if file specified in insatiation or with changeDirectory
     * is executable given system permissions.
     * @return true if executable false if not executable
     */
    public boolean isExecutable() {

        return this.IsExecutable;
    }



    /**
     * Checks system permissions on files and sets class flags appropriately.
     * Sets: private boolean IsReadable, IsWriteable, IsExecutable, IsDirectoryExecutable
     */
    public void checkSystemPermissions() {
        // checks if all directories to and including root are executable
        // checks to see if file is executable

        String dirPerms = "";
        File myFile = new File(AbsolutePath);

        String myPath = myFile.getPath();

        myPath = myPath.substring(0, myPath.lastIndexOf("/") + 1);

        boolean isPathExecutable = true;
        String checkPath = "";
        while (!myPath.equals(checkPath)) {
            checkPath = myPath.substring(0, myPath.indexOf("/", checkPath.length()) + 1);

            String[] myExecString = {"ls", "-ald", checkPath};

            try {
                Process p = Runtime.getRuntime().exec(myExecString);
                BufferedReader myBuf = new BufferedReader(new InputStreamReader(p.getInputStream()));
                dirPerms = myBuf.readLine();

                if (!dirPerms.substring(0, 1).equals("d") || !dirPerms.substring(3, 4).equals("x")) {
                    isPathExecutable = false;
                    break;
                }
            } catch (IOException ex) {
                isPathExecutable = false;
                Logger.getLogger(Auth.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

        IsDirectoryExecutable = isPathExecutable;


        /* dirPerms comes out of the loop with the result of the last
         * directory ls still valid
         * File paramters:
         * d r w x
         * 0 1 2 3
         */
        IsDirectoryReadable = dirPerms.substring(1, 2).equals("r");
        IsDirectoryWritable = dirPerms.substring(2, 3).equals("w");
        IsDirectoryExecutable = dirPerms.substring(3, 4).equals("x");


        // If the path to the file is not executable, the file itself
        // is not readable, writable or executable
        if (!IsDirectoryExecutable) {
            return;
        }

        // get system information for file at end of path
        // If the file does not exist, figure out if the directory is writeable
        if (myFile.exists()) {

            String[] myExecString = {"ls", "-ald", myFile.getPath().toString()};

            String filePerms = "";
            try {
                Process p = Runtime.getRuntime().exec(myExecString);
                BufferedReader myBuf = new BufferedReader(new InputStreamReader(p.getInputStream()));
                filePerms = myBuf.readLine();
                if (!filePerms.substring(0, 1).equals("-")) {
                    // Got something strange, other than a file listing
                    IsReadable = false;
                    IsWriteable = false;
                    IsExecutable = false;
                    return;
                }
            } catch (IOException ex) {
                IsReadable = false;
                IsWriteable = false;
                IsExecutable = false;
                Logger.getLogger(Auth.class.getName()).log(Level.SEVERE, null, ex);
                return;
            }

            /* If the file exists
             * readable: directory is executable & file is readble
             * writeable: directory is executable & writeable & file is writeable
             * executable: directory is executable & file is readable & executable
             * - r w x
             * 0 1 2 3
             */
            IsReadable = IsDirectoryExecutable && filePerms.substring(1, 2).toUpperCase().equals("R");
            IsWriteable = IsDirectoryExecutable && IsDirectoryWritable && filePerms.substring(2, 3).toUpperCase().equals("W");
            IsExecutable = IsDirectoryExecutable && filePerms.substring(1, 2).toUpperCase().equals("R") && filePerms.substring(3, 4).toUpperCase().equals("X");

        } else {
            /* If the file does not exist
             * readable: false
             * writeable: directory is executable & writeable
             * executable: false
             */

            IsReadable = false;
            IsWriteable = this.IsDirectoryExecutable && this.IsDirectoryWritable;
            IsExecutable = false;
        }
    }

    /**
     * Returns the AuthName from the <directory> or .htaccess file
     * @return the AuthName value from the <directory> or .htaccess file
     */
    public String getAuthName() {
        return AuthName;
    }

    /**
     * Returns the authorized user for the specified file.
     * @return the username if the directory is successfully authorized,
     * null if not authroized.
     */
    public String getAuthUser() {
        return AuthorizedUser;
    }


    static {
        /* keywords that can be either a tag in a <directory> tag directive or
         * a line in an .htaccess (or its equivalent) file.
         */

        authKeyWords.add("AuthType");
        authKeyWords.add("AuthName");
        authKeyWords.add("AuthUserFile");
        authKeyWords.add("Require");
    }
}
